{
 "cells": [
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.110581Z",
     "start_time": "2025-01-17T03:09:38.106903Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "execution_count": 8
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备数据"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.144422Z",
     "start_time": "2025-01-17T03:09:38.136640Z"
    }
   },
   "source": [
    "from sklearn.datasets import fetch_california_housing\n",
    "\n",
    "housing = fetch_california_housing()\n",
    "print(housing.DESCR)\n",
    "print(housing.data.shape)\n",
    "print(housing.target.shape)"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".. _california_housing_dataset:\n",
      "\n",
      "California Housing dataset\n",
      "--------------------------\n",
      "\n",
      "**Data Set Characteristics:**\n",
      "\n",
      ":Number of Instances: 20640\n",
      "\n",
      ":Number of Attributes: 8 numeric, predictive attributes and the target\n",
      "\n",
      ":Attribute Information:\n",
      "    - MedInc        median income in block group\n",
      "    - HouseAge      median house age in block group\n",
      "    - AveRooms      average number of rooms per household\n",
      "    - AveBedrms     average number of bedrooms per household\n",
      "    - Population    block group population\n",
      "    - AveOccup      average number of household members\n",
      "    - Latitude      block group latitude\n",
      "    - Longitude     block group longitude\n",
      "\n",
      ":Missing Attribute Values: None\n",
      "\n",
      "This dataset was obtained from the StatLib repository.\n",
      "https://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html\n",
      "\n",
      "The target variable is the median house value for California districts,\n",
      "expressed in hundreds of thousands of dollars ($100,000).\n",
      "\n",
      "This dataset was derived from the 1990 U.S. census, using one row per census\n",
      "block group. A block group is the smallest geographical unit for which the U.S.\n",
      "Census Bureau publishes sample data (a block group typically has a population\n",
      "of 600 to 3,000 people).\n",
      "\n",
      "A household is a group of people residing within a home. Since the average\n",
      "number of rooms and bedrooms in this dataset are provided per household, these\n",
      "columns may take surprisingly large values for block groups with few households\n",
      "and many empty houses, such as vacation resorts.\n",
      "\n",
      "It can be downloaded/loaded using the\n",
      ":func:`sklearn.datasets.fetch_california_housing` function.\n",
      "\n",
      ".. rubric:: References\n",
      "\n",
      "- Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions,\n",
      "  Statistics and Probability Letters, 33 (1997) 291-297\n",
      "\n",
      "(20640, 8)\n",
      "(20640,)\n"
     ]
    }
   ],
   "execution_count": 9
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.147608Z",
     "start_time": "2025-01-17T03:09:38.144422Z"
    }
   },
   "source": [
    "# print(housing.data[0:5])\n",
    "import pprint  #打印的格式比较 好看\n",
    "\n",
    "pprint.pprint(housing.data[0:5])\n",
    "print('-'*50)\n",
    "pprint.pprint(housing.target[0:5])"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[ 8.32520000e+00,  4.10000000e+01,  6.98412698e+00,\n",
      "         1.02380952e+00,  3.22000000e+02,  2.55555556e+00,\n",
      "         3.78800000e+01, -1.22230000e+02],\n",
      "       [ 8.30140000e+00,  2.10000000e+01,  6.23813708e+00,\n",
      "         9.71880492e-01,  2.40100000e+03,  2.10984183e+00,\n",
      "         3.78600000e+01, -1.22220000e+02],\n",
      "       [ 7.25740000e+00,  5.20000000e+01,  8.28813559e+00,\n",
      "         1.07344633e+00,  4.96000000e+02,  2.80225989e+00,\n",
      "         3.78500000e+01, -1.22240000e+02],\n",
      "       [ 5.64310000e+00,  5.20000000e+01,  5.81735160e+00,\n",
      "         1.07305936e+00,  5.58000000e+02,  2.54794521e+00,\n",
      "         3.78500000e+01, -1.22250000e+02],\n",
      "       [ 3.84620000e+00,  5.20000000e+01,  6.28185328e+00,\n",
      "         1.08108108e+00,  5.65000000e+02,  2.18146718e+00,\n",
      "         3.78500000e+01, -1.22250000e+02]])\n",
      "--------------------------------------------------\n",
      "array([4.526, 3.585, 3.521, 3.413, 3.422])\n"
     ]
    }
   ],
   "execution_count": 10
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.154034Z",
     "start_time": "2025-01-17T03:09:38.147608Z"
    }
   },
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#拆分训练集和测试集，random_state是随机种子,同样的随机数种子，是为了得到同样的随机值\n",
    "x_train_all, x_test, y_train_all, y_test = train_test_split(\n",
    "    housing.data, housing.target, random_state = 7)\n",
    "x_train, x_valid, y_train, y_valid = train_test_split(\n",
    "    x_train_all, y_train_all, random_state = 11)\n",
    "# 训练集\n",
    "print(x_train.shape, y_train.shape)\n",
    "# 验证集\n",
    "print(x_valid.shape, y_valid.shape)\n",
    "# 测试集\n",
    "print(x_test.shape, y_test.shape)\n",
    "\n",
    "dataset_maps = {\n",
    "    \"train\": [x_train, y_train],\n",
    "    \"valid\": [x_valid, y_valid],\n",
    "    \"test\": [x_test, y_test],\n",
    "}\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(11610, 8) (11610,)\n",
      "(3870, 8) (3870,)\n",
      "(5160, 8) (5160,)\n"
     ]
    }
   ],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.159059Z",
     "start_time": "2025-01-17T03:09:38.154034Z"
    }
   },
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "scaler = StandardScaler()\n",
    "scaler.fit(x_train)"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "StandardScaler()"
      ],
      "text/html": [
       "<style>#sk-container-id-2 {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: #000;\n",
       "  --sklearn-color-text-muted: #666;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-icon: #696969;\n",
       "\n",
       "  @media (prefers-color-scheme: dark) {\n",
       "    /* Redefinition of color scheme for dark theme */\n",
       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-icon: #878787;\n",
       "  }\n",
       "}\n",
       "\n",
       "#sk-container-id-2 {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip: rect(1px 1px 1px 1px);\n",
       "  clip: rect(1px, 1px, 1px, 1px);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       "#sk-container-id-2 div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       "#sk-container-id-2 label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "  align-items: start;\n",
       "  justify-content: space-between;\n",
       "  gap: 0.5em;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 label.sk-toggleable__label .caption {\n",
       "  font-size: 0.6rem;\n",
       "  font-weight: lighter;\n",
       "  color: var(--sklearn-color-text-muted);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable__content {\n",
       "  max-height: 0;\n",
       "  max-width: 0;\n",
       "  overflow: hidden;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  max-height: 200px;\n",
       "  max-width: 100%;\n",
       "  overflow: auto;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       "#sk-container-id-2 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       "#sk-container-id-2 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-label label.sk-toggleable__label,\n",
       "#sk-container-id-2 div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       "#sk-container-id-2 div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       "#sk-container-id-2 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       "#sk-container-id-2 div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  display: inline-block;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       "#sk-container-id-2 div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       "#sk-container-id-2 div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 0.5em;\n",
       "  text-align: center;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       "#sk-container-id-2 a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "#sk-container-id-2 a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "</style><div id=\"sk-container-id-2\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>StandardScaler()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-2\" type=\"checkbox\" checked><label for=\"sk-estimator-id-2\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow\"><div><div>StandardScaler</div></div><div><a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.6/modules/generated/sklearn.preprocessing.StandardScaler.html\">?<span>Documentation for StandardScaler</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></div></label><div class=\"sk-toggleable__content fitted\"><pre>StandardScaler()</pre></div> </div></div></div></div>"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 12
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 构建数据集\n",
    "\n",
    "这里我们构建多输入的数据集，注意到数据集介绍里对每个特征定义如下：\n",
    "\n",
    "> **Attribute Information**:\n",
    "> - MedInc        median income in block group\n",
    "> - HouseAge      median house age in block group\n",
    "> - AveRooms      average number of rooms per household\n",
    "> - AveBedrms     average number of bedrooms per household\n",
    "> - Population    block group population\n",
    "> - AveOccup      average number of household members\n",
    "> - Latitude      block group latitude\n",
    "> - Longitude     block group longitude\n",
    "\n",
    "我们认为最后两维作为位置信息要单独处理，故制作数据集如下"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.164032Z",
     "start_time": "2025-01-17T03:09:38.159059Z"
    }
   },
   "source": [
    "from torch.utils.data import Dataset\n",
    "\n",
    "class HousingDataset(Dataset):\n",
    "    def __init__(self, mode='train'):\n",
    "        self.x, self.y = dataset_maps[mode]\n",
    "        self.x = torch.from_numpy(scaler.transform(self.x)).float()\n",
    "        self.y = torch.from_numpy(self.y).float().reshape(-1, 1)\n",
    "            \n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "    \n",
    "    def __getitem__(self, idx):\n",
    "        return (self.x[idx],self.x[idx][-2:]), self.y[idx] #返回的是一个元组，元组里面是两个元素，第一个元素是一个元组，第二个元素是一个数。self.x[idx][-2:]代表取最后两个元素\n",
    "    \n",
    "    \n",
    "train_ds = HousingDataset(\"train\")\n",
    "valid_ds = HousingDataset(\"valid\")\n",
    "test_ds = HousingDataset(\"test\")"
   ],
   "outputs": [],
   "execution_count": 13
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.169524Z",
     "start_time": "2025-01-17T03:09:38.164032Z"
    }
   },
   "source": [
    "train_ds[1]"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((tensor([-0.2981,  0.3523, -0.1092, -0.2506, -0.0341, -0.0060,  1.0806, -1.0611]),\n",
       "  tensor([ 1.0806, -1.0611])),\n",
       " tensor([1.5140]))"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 14
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.172877Z",
     "start_time": "2025-01-17T03:09:38.170528Z"
    }
   },
   "source": [
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "batch_size = 8\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=False)\n",
    "val_loader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 15
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.186590Z",
     "start_time": "2025-01-17T03:09:38.183393Z"
    }
   },
   "source": [
    "#回归模型我们只需要1个数\n",
    "\n",
    "class WideDeep(nn.Module):\n",
    "    def __init__(self, input_dim=(8,2)):\n",
    "        super().__init__()\n",
    "        self.deep = nn.Sequential(\n",
    "            nn.Linear(input_dim[1], 30),\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(30, 30),\n",
    "            nn.ReLU()\n",
    "            )\n",
    "        # pytorch 需要自行计算输出输出维度\n",
    "        self.output_layer = nn.Linear(30 + input_dim[0], 1)\n",
    "        \n",
    "        # 初始化权重\n",
    "        self.init_weights()\n",
    "        \n",
    "    def init_weights(self):\n",
    "        \"\"\"使用 xavier 均匀分布来初始化全连接层的权重 W\"\"\"\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, nn.Linear):\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                nn.init.zeros_(m.bias)\n",
    "        \n",
    "    def forward(self, x_wide, x_deep):\n",
    "        # x_deep.shape [batch size, 6]\n",
    "        deep_output = self.deep(x_deep)\n",
    "        # concat [batch size, 30] with [batch size 8]\n",
    "        concat = torch.cat([x_wide, deep_output], dim=1)\n",
    "        logits = self.output_layer(concat)\n",
    "        # logits.shape [batch size, 1]\n",
    "        return logits"
   ],
   "outputs": [],
   "execution_count": 16
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.196284Z",
     "start_time": "2025-01-17T03:09:38.193593Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 17
  },
  {
   "cell_type": "code",
   "source": [
    "model = WideDeep()\n",
    "model"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.200647Z",
     "start_time": "2025-01-17T03:09:38.196284Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "WideDeep(\n",
       "  (deep): Sequential(\n",
       "    (0): Linear(in_features=2, out_features=30, bias=True)\n",
       "    (1): ReLU()\n",
       "    (2): Linear(in_features=30, out_features=30, bias=True)\n",
       "    (3): ReLU()\n",
       "  )\n",
       "  (output_layer): Linear(in_features=38, out_features=1, bias=True)\n",
       ")"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 18
  },
  {
   "cell_type": "code",
   "source": [
    "for name, param in model.named_parameters():\n",
    "      print(name, param.shape)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.207754Z",
     "start_time": "2025-01-17T03:09:38.204332Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "deep.0.weight torch.Size([30, 2])\n",
      "deep.0.bias torch.Size([30])\n",
      "deep.2.weight torch.Size([30, 30])\n",
      "deep.2.bias torch.Size([30])\n",
      "output_layer.weight torch.Size([1, 38])\n",
      "output_layer.bias torch.Size([1])\n"
     ]
    }
   ],
   "execution_count": 19
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:38.228538Z",
     "start_time": "2025-01-17T03:09:38.226009Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    for (datas_deep, datas_wide), labels in dataloader:\n",
    "        datas_deep = datas_deep.to(device)\n",
    "        datas_wide = datas_wide.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas_deep, datas_wide)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "    return np.mean(loss_list)\n"
   ],
   "outputs": [],
   "execution_count": 20
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:52.965368Z",
     "start_time": "2025-01-17T03:09:38.228538Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for (datas_deep, datas_wide), labels in train_loader:#和上面自定义的dataset是一致的\n",
    "                datas_deep = datas_deep.to(device)\n",
    "                datas_wide = datas_wide.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas_deep, datas_wide)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    " \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "\n",
    "                    # 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(-val_loss)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 10\n",
    "\n",
    "\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.MSELoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.0)\n",
    "\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5, min_delta=1e-3)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/14520 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "becc488fbacf4088afd5ed9f4a3d42fa"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=2000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=2000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=2000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n"
     ]
    }
   ],
   "execution_count": 21
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:53.025878Z",
     "start_time": "2025-01-17T03:09:52.965878Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        plt.plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        plt.plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        plt.grid()\n",
    "        plt.legend()\n",
    "        # plt.xticks(range(0, train_df.index[-1], 10*sample_step), range(0, train_df.index[-1], 10*sample_step))\n",
    "        plt.xlabel(\"step\")\n",
    "\n",
    "        plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=500)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGwCAYAAAAJ/wd3AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAVh9JREFUeJzt3QeYU1XaB/B/Mj1TaMMwlAGG3nuRIoI0wYLoWlERu+Kqq2thXRFEQd3VxYK66mdZFesKuogK0nvvvXcY6vSSmdzveU9yQ2aYPun3//O5pl1u7skkuW/Oec97TZqmaSAiIiLyErO3noiIiIhIMPggIiIir2LwQURERF7F4IOIiIi8isEHEREReRWDDyIiIvIqBh9ERETkVaHwMzabDcePH0dsbCxMJpOvd4eIiIjKQcqGpaeno169ejCbzYEVfEjgkZSU5OvdICIioko4cuQIGjRoEFjBh/R46DsfFxfn1m1brVbMmTMHQ4YMQVhYGIyC7Wa7jcKobWe72W5/kJaWpjoP9ON4QAUf+lCLBB6eCD4sFovarj/9wTyN7Wa7jcKobWe72W5/Up6UCSacEhERkVcx+CAiIiKvYvBBREREXuV3OR9ERBScCgoKVL6Cu8i2QkNDkZOTo7ZtFFYftjs8PLzMabTlweCDiIg8Xv/h5MmTuHDhgtu3m5iYqGZHGqkulObDdkvgkZycrIKQqmDwQUREHqUHHgkJCWqWhrsOmFKUMiMjAzExMW75NR4obD5qt14E9MSJE2jYsGGV/o4MPoiIyGNkWEAPPGrVquX2g2FeXh4iIyMNF3zk+ajdtWvXVgFIfn5+lab5GuevRUREXqfneEiPBwW+cMdwS1VzTRh8EBGRxxkpJyOYmdz0d2TwQURERF7F4IOIiIj8O/hYvHgxrr32WnXKXOl+mTlzZqGxvWeffRbt27dHdHS0Wueuu+5SySlERERG1bhxY0ydOtUt21q4cCFq1Kjh9qnLfh18ZGZmomPHjpg2bdolj2VlZWH9+vV44YUX1OWPP/6IXbt24brrroOv5RfYcCotB2dyfL0nREQUCPr3748nnnjCLdtas2YNHnjgAbdsKxhUeKrtsGHD1FKcatWqYe7cuYXue/fdd9GjRw8cPnxYzQsuKjc3Vy2up+TVe1HcWQlv18l0XDNtBWJCQ3Dbte7bbiDQX0d3vp6BgO02VruN3HZ/brfskxTFkumhsriTbFe/dPe2XZ+jpG3LYzLrQ6qNlkWfZuyO/dS80O6SyPPJ88rfNSQkpNBjFXn/ebzOR2pqqhqeqV69erGPT5kyBRMnTrzk/jlz5rh1atZZ1eMRilwbLgmQjILtNhajttvIbffHdsuBWapxSlEsqU0h5OCVY3XfQTP7bPmHHyLDzOWasfHII49g0aJFann77bfVfdLjP3bsWHz33Xd45ZVXsH37dtXDX79+fTz//PNYu3atGgFo0aIFxo8fr3pOdB06dMDDDz+sFiHDJm+99ZY61s2fPx9169bFpEmTMHz4cJQlOztbXcprqgcAP//8szqe7t+/H3Xq1FG9LI8++qjz33z88cd4//33cezYMcTFxaFXr174/PPP1WM//fQTXnvtNRw4cABRUVFqX7/66iuVPlGU/A3l+SUFQ2p9uJK2+0XwIXXnJQfktttuU40tzrhx4/Dkk08W6vlISkrCkCFDSvw3lXEuMw8vbVgIq82EAQMHIioiAkYh0ah8KQ0ePLhKRWECDdttrHYbue3+3G45DkgZcKnGKUWxRFZePjq/5ptAaeuEwbCEl33ok0Dj4MGDaNu2rfMH8rZt29Tlyy+/jNdffx1NmjRRQYS0T3IhX331VUREROCLL75Qx70dO3Y4e/ylGJi03/W49o9//EP9mzfffFONEjz44IMqAKhZs2ap+yYBgpDXVLa3bt06jBkzBi+++CJuvvlmLF++XAUeknd59913q6DoueeeU8FG7969ce7cOSxdulT9W6lWet9996ng4/rrr0d6erp6LDY2Vm2/uL+nPH+/fv2cf8+iIxc+DT7kwyAvgkS4Em2VRP5QshQlHyB3foiqRV9Mb8nXzH73AfUGd7+mgYLtNh6jtt0f2y3DEtLTIAdfvRqnL6uRuu5HaSSokIJa+uQJsXv3bnX50ksvYejQoc514+Pj0blzZ+dtCU5kMsasWbMK9T7or4NOAoNRo0ap69Jr8c4776hA4aqrrkJp9J4bfXuSyDpw4EDV2yJatWqFnTt34o033sA999yDo0ePqnZI/qUEFXJulq5du6p1T506pXowbrzxRjRq1EjdJ3mdJZHnk+ct7r1WkfdeqCcDj0OHDqnuJHf2YFRWRKgZoWYT8m0aMvMKUHpcSUREnhIVFoLtL108eFcl/yA9LR2xcbHlDmjkuauqW7duhW7L8MeECRPwyy+/qJ4EOZjL0ITkOpamQ4cOzusSHMixMiUlpcL7Iz0sI0aMKHRfnz59VFAiwZ/0iElgIT01EtjIMnLkSJXaIIGGBC4yS1UCKhl1+NOf/qSCL08yeyrw2LNnD/744w+31/KvLInULOH2N11mrnFOvUxE5G/s38ehblmiwkMqtL47KnQWzYX461//ihkzZmDy5MlYsmQJNm7cqA7meo5LScKK9BTIvnkigVR6O2QG6tdff61yS6SHRIIOmaorOSMyZPfrr7+iTZs2qvelZcuWavjHr4IPifDkhZVFyA7KdYnwJPCQiEm6jSRZRSIuOZuhLGX9EbxBDz5kvJGIiKg0MuxSnnOYLFu2TA2hSG+CBB2SYCv5It7SunVrtQ9F90kSX/WEVEn8HTRokMpV2bx5s9o/GZnQgx7pKZHclg0bNqh2SzDlSRUedpHAYsCAAc7berLo6NGjVbeTZNyKTp06Ffp3CxYsKJT56wvREdLcXGTlseeDiIjKLgy2atUqdaCW5MuSeiWaN2+uZr1I0qkcyKXWlTenwD711FPo3r27mi1zyy23YMWKFSqB9b333lOPS+6JzIKRJFEZTpk9e7baP+nhkPbNmzdPDbfImYfl9unTp1VA41fBhwQQ+hzj4pT2mK9FO3o+MnLZ80FERKWT4RT5YS3DEZLD8emnnxa7nsxWkcROmUkiyacyy7MiMz+qqkuXLmr6rwynSAAiQyuSFCu9MUJKXUhwJB0EMltFgiUZgpGZPJIvItNmJT9E9llyQyRRtaR6Xu7i8Tof/uTisAt7PoiIqHQybCG9CK70A3rRHhJ9CEMn9UBcFR2G0Yr5oV7ecunSCXD+/PlCkzlktoosxenbt68qyV4c6eH47bff4G2GOrGcfdiFwQcREZEvGSr4uDjbhcMuRETknx566CGVY1LcIo8Fg1Aj9nxInQ8iIiJ/9NJLL6l8k+L4Q90sdzBW8MGcDyIi8nMJCQlqKYm3TybnCRx2ISIiIq8yVPDBhFMiIiLfM1TwwZ4PIiIi3zNY8MGeDyIiIl8zVPARo/d8MPggIiLyGUMFH5YIDrsQEZF3SOVTKVteHiaTCTNnzoRRGCv44LALERGRzxkq+GCdDyIiIt8zG7PCKYddiIh8Rk6qlpfpnsWaVbH1y3nm9Q8//BD16tW7pKDXiBEj1Bls9+3bp67XqVNHlT2XU9r/8ccfbnuJtmzZgiuvvBJRUVGoVasWHnjgAWRkZDgfX7p0KS677DJER0ers9b26dMHhw4dUo9t2rQJAwYMQGxsrKqI2rVrV6xduxb+JNSIU22tBRry8m0IDzVU7EVE5B8kYJhcr8qbkW/w6hX9R387DoRHl7naTTfdhD//+c9YsGABBg4cqO47d+6cOgPs7NmzVSAwfPhwvPLKK4iIiMB//vMfXHvttdi1axcaNmyIqsjMzMTQoUPRq1cvrFmzBikpKbjvvvvw6KOP4rPPPkN+fj5GjRqF+++/H19//TXy8vKwevVqlTci5LHOnTvj/fffR0hICDZu3IiwsDD4E0MGHyIrLx/hoeE+3R8iIvJPNWrUwLBhwzB9+nRn8PHDDz8gPj5e9SqYzWZ07NjRuf6kSZMwY8YM/PzzzypIqIrp06cjJydHBTTSsyHeffddFdy89tprKqBIS0vD1VdfjaZNm6rHW7du7fz3hw8fxtNPP41WrVqp282bN4e/MVTwERZiRqhJQ75mQkZuPqpbGHwQEXldmMXeA1FFMiSSlp6OuNhYFQyU+7nLSe9deO+991TvxldffYVbb71VPZf0fEyYMAG//PILTpw4oXojsrOz1YG/qnbs2KECGz3wEDKsIu2VnpW+ffvi9ttvV8HR4MGDMWjQINx8882oW7euWvfJJ59UPSVffPGFekx6cfQgxV8YbtzBMduWSadERL4iwwMy9OGORYKJiqzvGJooD+lp0DRNBRhHjhzBkiVLVEAi5Kyz0tMxefJkdb8MbbRv314NgXjDtGnTsGzZMvTu3RvffvstWrRogZUrV6rHJCjatm2b6hmZP38+2rRpo/bVnxg2+GCtDyIiKk1kZCRuuOEG1eMhuRUtW7ZEly5d1GNy4L/77rsxcuRIFXQkJibi4MGDbnne1q1bq6RRyf3QyfNJj4vsg07yOsaNG4fly5ejXbt2arhGJ8HIX/7yF8yZM0e14dNPP4U/MV7w4WhxZi57PoiIqHTS0yE9H5988omz10PPo/jxxx9Vj4cECjIM4q5T3Y8aNUoFPqNHj8bWrVtV0qskv955551qds2BAwcwceJErFixQs1wkQBjz549KmiRoR/JOVm4cKF6TIIWSVp1zQnxB4bK+SjU88HptkREVAaZ7lqzZk2VayEBhu7NN99UU25l2EOSUJ999lmVBOoOFosFv//+Ox5//HE1hVdu33jjjeo59ccl2JBcjrNnz6pcj7Fjx+LBBx9UuSdy31133YVTp06pfZOeDwlW/IkBgw+Z421Ss12IiIhKI0Mdx48fL7Z0uuRTuJIAwFVFhmG0IvVHZCin6PZ10vvx5ZdfqhoeRRNtw8PD1RCRvzMbtecjg8MuREREPmHY4COLCadEROQFkrAqVVCLW9q2bQsjCjVswimn2hIRkRdcd9116NmzZ7GPhflZ5VFvMV7wwam2RETkRXKOFVnI0MMu9qQeJpwSEXmPu6ahkm8VTYytLMP2fDDhlIjI82T2hT5jpHbt2uq2fgI0dwQ0UlFUzoNS7vLqQcDmo3ZL4HH69Gn196vqcJHxgg/H34kJp0REnicHx+TkZHX+k+KmrFb1YChFteS08+4KaAKB5sN2y/M1aNBAndyuKowXfLDIGBGRV0lvh5xmXgpgFRS4r9fZarVi8eLF6Nevn6ESN60+bLc8X1UDD2MHHxx2ISLyGr2r3p0HSzkISkAjpciNFHyEBEG7jTNI5hBhtifLsOeDiIjINwxcZIw9H0RERL5g2OCDdT6IiIh8w7jBR16+2+YrExERUfkZNviwaUBuPoveEBEReZvhgo9wlxZncOiFiIjI6wwXfJhNgCXc3v3BpFMiIiLvM1zwIfTgg9NtiYiIvM/YwQeHXYiIiLzOkMFHdLi9sGtmHoddiIiIvM2YwYdjygtPLkdEROR9hh524WwXIiIi7zNk8BHtGHbJ4rALERGR1xky+LA4hl0424WIiCgAgo/Fixfj2muvRb169dQpkmfOnFnocSlZPn78eNStWxdRUVEYNGgQ9uzZA39i0RNOOexCRETk/8FHZmYmOnbsiGnTphX7+Ouvv463334bH3zwAVatWoXo6GgMHToUOTk58Bcxzqm2HHYhIiLyNnsXQAUMGzZMLcWRXo+pU6fi73//O0aMGKHu+89//oM6deqoHpJbb70V/oB1PoiIiAIo+CjNgQMHcPLkSTXUoqtWrRp69uyJFStWFBt85ObmqkWXlpamLq1Wq1rcSd9eRKhJXWbkuP85/JHeRiO01RXbbax2G7ntbDfb7Q8qsj9uDT4k8BDS0+FKbuuPFTVlyhRMnDjxkvvnzJkDi8UCTzi4ZyeAEBw8dgKzZx+DUcydOxdGxHYbj1HbznYby1w/a3dWVpZvgo/KGDduHJ588slCPR9JSUkYMmQI4uLi3B6VyR+re+cOmL5vG6Kr1cTw4T0Q7PR2Dx48GGFhYTAKtttY7TZy29luttsf6CMXXg8+EhMT1eWpU6fUbBed3O7UqVOx/yYiIkItRckL6qkXNc5if76sPJtf/eE8zZOvqT9ju43HqG1nu40lzM/aXZF9cWudj+TkZBWAzJs3r1AkJLNeevXqBX8R7Ug4zWKdDyIiIq+rcM9HRkYG9u7dWyjJdOPGjahZsyYaNmyIJ554Ai+//DKaN2+ugpEXXnhB1QS5/vrr4W91PjI41ZaIiMj/g4+1a9diwIABztt6vsbo0aPx2Wef4ZlnnlG1QB544AFcuHABffv2xW+//YbIyEj43Ynl2PNBRETk/8FH//79VT2PkkjV05deekkt/urisEsBbDYNZrN96i0RERF5njHP7eIYdhFZVg69EBEReZMhg4/IMDP0zo4sVjklIiLyKkMGHzI0FK2fXC6PPR9ERETeZMjgQ1gcSac8vwsREZF3GTb4iI5w9Hww+CAiIvIq4wYfjmEXmfFCRERE3mPY4MPimG6bwZ4PIiIirzJs8BHjGHZhoTEiIiLvMmzwYXEEHyyxTkRE5F2GDT6cVU457EJERORVxg0+9NkuTDglIiLyKrPRez441ZaIiMi7zEbP+chkwikREZFXmY0+7JLFhFMiIiKvMm7woQ+7sOeDiIjIq4wbfLC8OhERkU8YN/hgeXUiIiKfMBv9rLYsr05ERORdhg0+LpZXZ88HERGRN5mNfmI55nwQERF5l2GDj2hHzkduvg35BTZf7w4REZFhGDf4cAy7CJZYJyIi8h7DBh/hoWaEhZjU9SzW+iAiIvIawwYfwuIYemHeBxERkfcYOvjQZ7xkssQ6ERGR1xg6+HDOeOGwCxERkdcYO/hgzwcREZHXGTr4iHFUOWXCKRERkfcYOvjQE05ZYp2IiMh7DB18RDtyPrI47EJEROQ1xg4+9JwPDrsQERF5DYMP1vkgIiLyKmMHH3qRMZZXJyIi8hpjBx/6bBf2fBAREXmNoYOPi7Nd2PNBRETkLYYOPpw9H0w4JSIi8hpjBx/M+SAiIvI6QwcfFkfPB2e7EBEReY+hgw/9rLZMOCUiIvIeQwcfesIph12IiIi8x9DBR7TLsIumab7eHSIiIkMwePBh7/nIt2nIK7D5eneIiIgMwdDBhyXM3vMheHI5IiIi7zB08BEaYkZEqP0lyGDSKRERkVcYOvgoNOOFSadEREReYfjgw1nrg1VOiYiIAjP4KCgowAsvvIDk5GRERUWhadOmmDRpkt/OJonWp9ty2IWIiMgr7EdeN3rttdfw/vvv4/PPP0fbtm2xdu1ajBkzBtWqVcNjjz0Gf53xksmEUyIiosAMPpYvX44RI0bg6quvVrcbN26Mr7/+GqtXry52/dzcXLXo0tLS1KXValWLO+nbc91uVJi98yctK9ftz+cvimu3EbDdxmq3kdvOdrPd/qAi+2PS3DweMnnyZHz44YeYM2cOWrRogU2bNmHIkCF48803MWrUqEvWnzBhAiZOnHjJ/dOnT4fFYoGnfbLLjE3nzPhTcgEuT/TPoSEiIiJ/l5WVhdtvvx2pqamIi4vzbvBhs9nwt7/9Da+//jpCQkJUDsgrr7yCcePGlbvnIykpCWfOnClz5ysTlc2dOxeDBw9GWFiYuu+ZH7dixobjeHpIczxweTKCUXHtNgK221jtNnLb2W622x/I8Ts+Pr5cwYfbh12+++47fPXVV6rnQnI+Nm7ciCeeeAL16tXD6NGjL1k/IiJCLUXJC+qpF9V127GR9sucfM2v/oie4MnX1J+x3cZj1Laz3cYS5mftrsi+uD34ePrpp/Hcc8/h1ltvVbfbt2+PQ4cOYcqUKcUGH77GhFMiIqIAn2orYz5mc+HNyvCLDMf4o+hwe52PLNb5ICIi8gq393xce+21KsejYcOGathlw4YNKtn0nnvugT+yOOp8sLw6ERFRgAYf77zzjioy9sgjjyAlJUXlejz44IMYP348/BHLqxMREQV48BEbG4upU6eqJaDKq7Png4iIyCsMf26XaL28OnM+iIiIvILBhz7swtkuREREXmH44MPimO3Cng8iIiLvMHzwwTofRERE3sXgQ084zcuHmyvNExERUTEYfDgSTiXuyLH6ZyE0IiKiYGL44CMqzN7zIVhojIiIyPMMH3yYzSaWWCciIvIiwwcfwsKkUyIiIq9h8OFSYp3TbYmIiDyPwYdrrQ/mfBAREXkcgw/XEuscdiEiIvI4Bh9Fan0QERGRZzH4cEk4zeKwCxERkccx+FDDLnrPB4ddiIiIPI3BR6Hzu7Dng4iIyNMYfLgknGax54OIiMjjGHyonA/7sAvLqxMREXkegw+XImMsr05EROR5DD5UkTHW+SAiIvIWBh+us1047EJERORxDD5cZ7sw4ZSIiMjjGHy4VDhlzgcREZHnMfgolPPB4IOIiMjTGHy4zHZhwikREZHnMfhQPR/2YZdsawEKbJqvd4eIiCioMfhwSTgVzPsgIiLyLAYfACJCzQgxm9R1llgnIiLyLAYfAEwmk3PohUmnREREnsXgw4FJp0RERN7B4MPB2fPBnA8iIiKPYvBRtMoph12IiIg8isGHQ7ReaIwJp0RERB7F4KNoiXX2fBAREXkUg48iJdYzGHwQERF5FIOPIjkfrPNBRETkWQw+HKI524WIiMgrjBV85OciOvdUsQ9ZONuFiIjIKy6e1CTYHVqB0C9vQM+Q6gDGXPJwjDPhlMMuREREnmScno+EVkB+DmJzjgOpR0tMOOWwCxERkWcZJ/iIqgGtXld11bR/folTbVlenYiIyLOME3wA0JpeqS7N+xdc8lg0ez6IiIi8wljBRxN78GE6sAgoyC9+qi17PoiIiDzKWMFH3U7IC4mGKTcNOLa22OCDRcaIiIg8y1DBB8whSIltZ7++d16xdT6yOOxCREQUeMHHsWPHcMcdd6BWrVqIiopC+/btsXZt4Z4GXzkd195+Ze8fxdf5YIVTIiKiwKrzcf78efTp0wcDBgzAr7/+itq1a2PPnj2oUaMG/EGKHnwc3wBkngWia6mbMY6E07x8G6wFNoSFGKtTiIiIKGCDj9deew1JSUn49NNPnfclJyfDX+SE1YCW0AamlO2AzHpp/yd1f5Rj2EVPOq1mYfBBREQUEMHHzz//jKFDh+Kmm27CokWLUL9+fTzyyCO4//77i10/NzdXLbq0tDR1abVa1eJO+vbyG/dHWMp22PbMRUGrEeo+E4CwEBOsBRpSs3JgCUPQ0Nvt7tfT37Hdxmq3kdvOdrPd/qAi+2PSNE1z55NHRkaqyyeffFIFIGvWrMHjjz+ODz74AKNHj75k/QkTJmDixImX3D99+nRYLBZ4Qnz6NvTZ+xpyQqvh93ZvAyYJPYC/rQlBZr4J4zrmI9EzT01ERBSUsrKycPvttyM1NRVxcXHeDT7Cw8PRrVs3LF++3HnfY489poKQFStWlKvnQ4Ztzpw5U+bOVyYqmzt3LgYP6Ieod9rAZM2C9b6FQB37DJj+byzGsQs5+OHBnujYoBqChbPdgwcjLCyIunTKwHYbq91GbjvbzXb7Azl+x8fHlyv4cPuwS926ddGmTZtC97Vu3Rr//e9/i10/IiJCLUXJC+qpFzUsKgamxpcDe35H2MFFQIPOhWp9SJ0xf/qDuosnX1N/xnYbj1HbznYbS5iftbsi++L2rEqZ6bJr165C9+3evRuNGjWCX2k28JIpt3rwkclCY0RERB7j9uDjL3/5C1auXInJkydj7969Knfjww8/xNixY+FXmg2yXx5eCeRmqKvRjum2Waz1QUREFDjBR/fu3TFjxgx8/fXXaNeuHSZNmoSpU6di1KhR8Cs1mwDVGwE2K3BwqbrL4phuyxLrREREnuP2nA9xzTXXqMWvyQwXGXpZ+4l96KXlVYjRTy7HEutEREQeY+xKWvrQyz77eV4sEfaej0ye2ZaIiMhjjB18yIwXcyhwbr9aoh05H0w4JSIi8hxjBx+RcUDSZfbre+ddnO3ChFMiIiKPMXbwIZpdab/cN9+ZcMqcDyIiIs9h8NHUUe/jwGLEhtmLvXLYhYiIyHMYfCR2AKJrA3kZaJCxRd3FhFMiIiLPYfBhNgNN7UMv9c/Y631w2IWIiMhzGHy4DL3UPrVMXbLIGBERkecw+BCOno/o89tRGxdYXp2IiMiDGHyImNpA3Y7q6uXmzUw4JSIi8iAGH0WqnfYL2azqfGiafeYLERERuReDjyJ5H5ebt8BmK0Buvs3Xe0RERBSUGHzoknpAC49FLVM62pkOcuiFiIjIQxh86ELCYGpyhbraz7yZSadEREQewuCjmFkvV4RsQiZrfRAREXkEgw9Xzex5H11Me5Cdfs7Xe0NERBSUGHy4qtEYR831EWqyIfywveAYERERuReDjyI2R3RVl3FHF/p6V4iIiIISg48itsf0UJc1Ty4FWOuDiIjI7Rh8FHE0rjNytVBEZx8Hzuzx9e4QEREFHQYfRYRGxmK1rZX9xr55vt4dIiKioMPgo4jo8BAstnWw39jL4IOIiMjdGHwUER0RejH4OLgUsOb4epeIiIiCCoOPYoKPXVoSUkPjgfxs4PByX+8SERFRUGHwUcywC2DCdks3+x0ceiEiInIrBh9FWCJC1eWGcAYfREREnsDgo4jocHvwsSakI2AyA6d3AKnHfL1bREREQYPBRxHRETLsAqRYo4B6Xex3csotERGR2zD4KCbhVGTlFQDNBtnv5NALERGR2zD4KMKiEk6BzNx851lusX8BUJDv2x0jIiIKEgw+iohx9Hyo4EOGXSKrAzmpwPH1vt41IiKioMDgowiLI+E0y1oAmykEaNLf/gCHXoiIiNyCwUcJCadyQttsa8HFoZe9f/h2x4iIiIIEg48iosJCYDLZr2fm5QNNHcGHDLtknfPpvhEREQUDBh9FmEwmZ62PrNwCoFp9oHZrQLPZE0+JiIioShh8lDLjJUOSToVz6GW+D/eKiIgoODD4KGXGi6r14Rp8SLExSQYhIiKiSmPwUQyLI+lU5XyIhr2B0Cgg/QSQst23O0dERBTgGHyUMt1W1foQYZFA477265xyS0REVCUMPkobdpGEU53r0AsRERFVGoOP0kqs68MuQp9ye2g5kJfpoz0jIiIKfAw+yiqxrotvDlRrCBTkAQeX+W7niIiIAhyDj9JyPvTZLkIqjzW70n6d1U6JiIgqjcFHKSXWs1x7PkSzQfZL5n0QERFVGoOPYkQ7hl0yXBNORXI/QE42d3YvcP6gb3aOiIgowDH4KEa0I+E0yzXhVERWA5J62K9zyi0REZF/Bh+vvvqqOl/KE088gYDO+bhkyi1LrRMREfld8LFmzRr8+9//RocOHRCIwy6FZrsUnXK7fxFQYPXynhEREQU+jwUfGRkZGDVqFD766CPUqFEDgZhwWmzwUbcTYKkF5KUDR1Z7f+eIiIgCnP0nvgeMHTsWV199NQYNGoSXX365xPVyc3PVoktLS1OXVqtVLe6kb6+s7UY4QjIJPopbNyS5P8zb/ouC3XNhq+/IAfFj5W13sGG7jdVuI7ed7Wa7/UFF9scjwcc333yD9evXq2GXskyZMgUTJ0685P45c+bAYrF4Yvcwd+7cUh8/niX/D8X59CzMnj37kseT0muhC4D0DTOwKLszAkVZ7Q5WbLfxGLXtbLexzPWzdmdlqYNnuZg0zb3niD9y5Ai6deumXhQ916N///7o1KkTpk6dWq6ej6SkJJw5cwZxcXFuj8pkvwYPHoywsLAS1zt6PhsD3lyCyDAztox31PZwlXEKYW+1tW/ziR1AdG34s/K2O9iw3cZqt5Hbznaz3f5Ajt/x8fFITU0t8/jt9p6PdevWISUlBV26SN+AXUFBARYvXox3331XBRohIfacChEREaGWouQF9dSLWta2q0Xb47Ecqw0mcwhCQ4qkxtRoACS2B05uQdihJUDHWxAIPPma+jO223iM2na221jC/KzdFdkXtwcfAwcOxJYtWwrdN2bMGLRq1QrPPvtsocDD3xNORZa1AHFFgw+92unJLfZqpwESfBAREfkDtwcfsbGxaNeuXaH7oqOjUatWrUvu91fhIWaEmk3It2nIyi1AXGRY8VNul/7LXu/DZgPMrNdGRERUHjxiFkOKolkcVU4ziptuK5J6AuExQOZp4ORm7+4gERFRAPPYVFtXCxcuRKCJiQhFWk7+pSXWdaHh9nO97JptH3qp18nbu0hERBSQ2PNRAouzymkxJdZ1Ta+0X/I8L0REROXG4KMyJdZdk07FkVVAjr04GhEREZWOwUcZZ7bNLGnYRdRMBmo2AWz5wMEl3ts5IiKiAMbgo4wz22YVd2bb4k40t/cPL+wVERFR4GPwUYKY0k4uV9zQi+R9uLdYLBERUVBi8FGVhFPRuC9gDgMuHALO7ffOzhEREQUwBh9l5HyUONVWFxEDNOplv86hFyIiojIx+ChjtkuJRcaKzfvglFsiIqKyMPgoQXR5E05FM0fwITNe8i+eoZeIiIguxeCjBJbyJpyKOu2AmDqANQs4vMLzO0dERBTAGHyUUl69zDofOpOJQy9ERETlxOCjjDofZc52KTr0wuCDiIioVAw+qjrbRddkgHSBACnbgLQTnt05IiKiAMbgo8xzu5Sz5yO6FlCvs/26nOWWiIiIisXgowTResJpeXs+ilY7JSIiomIx+Cgz56MiwYcj72P/AsBWzh4TIiIig2HwUcawi7VAQ16+rXz/qH43IKIakH0eOL7BsztIREQUoBh8lJFwWqGk05BQoMkV9usceiEiIioWg48ShIaYERFqLn+J9Uum3PI8L0RERMVh8FGOoZdylVjX6cXGjq21D78QERFRIQw+SmFxDL1UKOm0ehIQ3xLQbMD+RZ7bOSIiogDF4KM8JdbLW+tDx6EXIiKiEjH4KE/PR0VqfbgGH/vmA5rmgT0jIiIKXAw+ypXzUcHgo1EfIDQSSDsGnN7pmZ0jIiIKUAw+ShHtKDSWUdFhl7AoewAiOOWWiIioEAYfpbA4SqxnVSTh9JKhFwYfRERErhh8lCJaL7Fekam2RafcHlwG5GW5d8eIiIgCGIOPcp3ZthI9H7VbAnENgIJc4NBy9+8cERFRgGLwUY4S6xVOOBUmE9DsSvt1Dr0QERE5MfgohaWydT50zQbZL1nvg4iIyInBRyliHAmnlRp2EclXAKYQ4Mxu4MJh9+4cERFRgGLwUQqLM+G0ksFHVHWgQTf7dU65JSIiUhh8lCJan2pbmdkuRYdemPdBRESkMPgoRbSzyFglez5cp9zKSeYKrO7ZMSIiogDG4KM85dUrm3Aq6nUComoCuWnA0bXu2zkiIqIAxeDDU3U+dOYQoOkA+3UOvRARETH4KE+dD0k41apydlp96IVTbomIiBh8lKfOh00DcvNtld+Qfp6X4xuBzLNu2jsiIqLAxOCjFJYwe89HlZNOYxOBOu0AaMD+Be7ZOSIiogDF4KMUZrMJFr3EelWSTl17Pzj0QkREBsfgw9OFxi7J+5gH2KowhENERBTgGHx4usS6ruFlQJgFyEwBTm11z84REREFIAYf5e75qOKwS2gEkNzPfp1TbomIyMAYfJS3xHpVez6KDr0QEREZFIOPchYaq9Jsl6JJp4dXArkZVd8eERFRAGLwUYZox7BLlU4up6vZBKjRGLBZgYNLqr49IiKiAOT24GPKlCno3r07YmNjkZCQgOuvvx67du1CoNKn2lZ5toswmVjtlIiIDM/twceiRYswduxYrFy5EnPnzoXVasWQIUOQmZkJw57fxVWzQfZL5n0QEZFB2Y+sbvTbb78Vuv3ZZ5+pHpB169ahXz/HbA8Xubm5atGlpaWpSwlaZHEnfXsV2W5UqEldpme7aX8aXIZQcyhM5w/AemqXfSjGwyrT7mDAdhur3UZuO9vNdvuDiuyPSavSGdPKtnfvXjRv3hxbtmxBu3ZSYrywCRMmYOLEiZfcP336dFgsFvja3GMmzDocgp61bbi9mXuKg/XZMxnxGTuxucFdOFDb0RNCREQUwLKysnD77bcjNTUVcXFxvgs+bDYbrrvuOly4cAFLly4tdp3iej6SkpJw5syZMne+MlGZDAUNHjwYYWFh5fo3X6w8jJd+2Ylhbevg7Vs7umU/zMvfQsiCSbA1H4qCm7+Cp1Wm3cGA7TZWu43cdrab7fYHcvyOj48vV/Dh9mEXV5L7sXXr1hIDDxEREaGWouQF9dSLWpFtx0aFq8vsfJv79qfFYGDBJJgPLoXZpAGh9ufwNE++pv6M7TYeo7ad7TaWMD9rd0X2xWNTbR999FHMmjULCxYsQIMGDRCoYhwJp1U+sZyrOu2B6ATAmgkcWem+7RIREQUAtwcfMoojgceMGTMwf/58JCcnI5BZ3FlkTGc2A02vtF/nrBciIjIYsyeGWr788kuVMCq1Pk6ePKmW7OxsBKJoR52PLHfU+Siu2imDDyIiMhi3Bx/vv/++Sjbp378/6tat61y+/fZbBHZ5dTcOuwjV82ECTm0B0k+5d9tERER+zO0Jpx6euet10c7y6m7u+YiOB+p2BE5sBPbNBzrd5t7tExER+Sme26UMFv2stnkFsNncHFg5q52y1DoRERkHg49yznYRWdYCz+R9SM+Hzc3bJiIi8lMMPsoQEWqG2V5hHVnunPEiGnQHwmOB7HP24RciIiIDYPBRBpPJ5Mz7yMxzc+9ESBjQ5Ar79b3z3bttIiIiP8Xgwxdnti126IVTbomIyBgYfFQg6dQjwUdTR/BxZDWQk+r+7RMREfkZBh/lEO2cbuuBpNAajYBazQGtANi/yP3bJyIi8jMMPsoh2tHz4dYS68VWO+WUWyIiCn4MPsoh2lOFxorW+5Apt0FWpM2Xpi3cjykbQ7DvdKavd4WIiFww+KjAyeUy3V1iXdeoDxASAaQeAc7s8cxzGMzag+fw1vy9OJltwjM/bkF+gc3Xu0RERA4MPsohxpMJpyLcAjTqbb/OoZcqy7EW4Nn/bnZ2Im0+moYPl+z39W4REZEDg49ysHiqzocrTrl1m3fn71VDLbVjwjGysf1vNnXuHuw6me7rXSMiIgYfFavz4bGcD9cptweXAtZszz1PkNt+PA0fLNqnrr94TWtckajhypa1kVdgw1+/3wQrh1+IiHyOwUc5RId7eLaLSGgNxNYD8nOAQ8s99zxBTPI6ZLgl36bhqraJGNq2DkwmYNKINqgWFYYtx1LxwUJ7YEJERL7D4KMCCadZnko4FXKUbHblxVkvVGH/t/SACjDiIkPx0oi2zvsTYiMw8Tr77bfn78GOE2k+3EsiImLwUZGEU08Ou7gOvTDptMIOnsnEm3N3q+t/v6YNEuIiCz0+olM9DGlTB9YCDU99x+EXIiJfYvBRkYRTTw67iCb9AZMZOL0TSD3q2ecKIjabpoZbcvNt6NssHjd1bVDsCQJfGdkeNSxh2H4iDdMW7PXJvhIREYOPcon2ZHl1V5aaQP2u9usceim3b9YcwaoD5xAVFoLJI9urQKM4tWMj8NKIds4ZMVuP8Vw6RBR4Nhy+gLnHTOoHV6Bi8OEP5dWLq3bKoZdyOZmagymzd6jrfx3aEg1rWUpd/5oOdTGsXaJKSpXZL3kB/OElIuM5diEb936xHrMOh+CteYHbg8vgo0JTbT3c8+Ga97F/IVDghWAngGmahr/P3IL03Hx0SqqOu3s3LvPfSK/IpOvboWZ0OHaeTMe781lRlogCQ4FNw5PfbkR6jv3Y8H/LDmLD4fMIRAw+ysHimGrr8ZwPUb8LEFkdyEkFjq3z/PMFsFmbT+CPHSkICzHhtRs7IMRc/HBLUfExEZjkGH6ZtnAfthxNDboKrxvPmrBkzxlsO56KlLQclpcnCgIfL9mvhpjlmNS6ug02DaoHVz7zgcb+k55KFePo+ZDxNfkSDw3xYMxmDgGaDgC2zbBXO23Y03PPFcDOZ+Zhws/b1PWxA5qhZWJshf791R3q4tetdVUA89T3G/G/P/dFRKg9yAz03qAnv9+CubtD8Onu9c77JQ2mhiUc8THhqBUdgfjYCHVdArHaMXK78P3B8FoQBZOtx1Lxzzm71PW/D28FHN2EN3dGqmrOU//Yg+eGtUIgYfBRgdkueon1alFmzw+9SPCxdx4w4G+efa4ANWnWdpzNzEOLOjF4pH+zSm1Dkk9X7j+L3acy8NYfe/DMVYH14S3O58sPYu6OFISYNDRPiMWZTCvOZeaqX0jnMvPUAmSUuZ0ejWvio7u6oZolzCv7TYE9FCB9juZy9jxSxUnPxhPfblSlAqR44p+61MOvpzZh0nVt8PD0jfhw8T51f+eGNRAoGHyUQ3ioWXXtyx9eSqxLtUyP0s/zIsMuWefss2DIaeGuFPy44Zj6NS/DLfL3qQzJ+3j5+vZ46Mt1qiT7kLaJKnckkH8ZTZ69U10f0ciG1+7tjbCwMHVwOJ+Vh7MZeTiTkauW0+lyefG262PyPl998Bwe/modPr+nB8I82dNHAe3w2Szc+ckqNdPsy/t6qp40cr9Xf92JvSkZasbelBs6OGf0DWqdgOs71cPMjcfx9A+bMevPfREZFhi9lgw+KpB0eiHL6p28j7h6QEIbIGU7sH8B0O5Gzz9ngJAZR8/P2Kqu39MnucqR/lXtElUBsp82Hldjp4H04S36uvz56w3qHDYDW9VGv+onnI9JLowcFGRpidgyh22kSuytH67E8n1nMf6nbZg8sl2J05fJuCRQveuTVTh0NkvdvvezNfj6gcsK9RSTe35sfbb8oLr+z5s6qh9NVqvV+fiL17bF0r1nVXDy1rw9eDZAenD5k6acop2FxryU2NPUUWpdhl7I6R+/7VRTzZJqRuGpIS3css0J17ZVvyjkw/uvP+xVUgPN+JlbceBMJupWi8SUkW1Vr1BlSJDRoUF1vH1rZ7WNr1cfVmXriYoGu2M+XYODZ7NQv3qUKt636Wgq/jx9A5Ob3ehsRq7q0RAym++KFrUvWadGdLj6gSD+vWgfNh65gEDA4KOiM148XWK9aL0PKTamad55Tj+39uA5/GflIXV9ysgObvuFZf/wtlfXP1q8H+sOBdbUtR/WHVXDUDLk/vZtnVViaVUNalMHzw9vra6/MnsH/th+yg17SsFAauM89MU61UMmv8K/uLcHPh7dHRGhZszbmYIXftqmetCoajRNw7gft6gh0uYJMaUmlMqQsfTgSm7X0wEy+4XBRwVrfXit56NhLyA0Ckg/YR9+MTj5MEkJdflOu7lbA/RtHu/W7Q9uUwc3dK4fUB9eIb01L8y0D0P9ZVALdG/svvyge/sm47YeDdVr/tg3G7D9OE/IZ3RyKoOnvt+EpXvPqB9kn97dHU1qx6Broxoq8NV7y97j2aOr7Lu1RzBn+ymVbzj11k5lDgdLD64Mre5xDL/4OwYfFaxyKgmnXhEWCSRfbr/OaqeqHLpMKZPhkeeHt/HIc8jYqZwBd/+ZTPzzd/uUNn8mAdKj09cj21qA3k1r4ZEBlZv1U9oQjJwduE+zWqrA3n2fr1E1Q8i4v8RfmrUd/9t0HKFmEz64oys6uiRoD22bqA6A4h+/78KP63l+qqqcKHPi/+w/Ov86pCXa1qtWrh7cV1yGXzb5+fALg49yivZ2zodrtdPdvwPpp+yFx/LzDDcMI7+4ZTaKmDSircemf8p2X73RPvzyf8sOqGEef/bKLztUldZa0eGYekunchdZqwiZ6fLe7V3RpHY0jqfm4P7/rEW2Nyr9UrlzAt5buBdD31qKD3eaccqDweH7i/Y5Ex/fuLkj+hWTfzC6d2M82K+Juv7MD5uxdM8Zj+1PsMovsKlptRLwX9akJu673P56locEgNd1rBcQxceYllzBYRepMLd072k11lkzOkJ98ct1dRljv17TEu6eQmT6lNtDy4A3XJIr5cy3oZH2JSzK5bpcRjkuL95nNkegzbHjMC/aBIRbLv6bQpcRLv/24jbO5Jix5EAGWifVQqu6ZUffnvggynCLnItFzslyVbu6Hn2+K1vVUWfF/X7dUfXh/fXxfohy5Pv4k9+2nsAXjvwXORAkxEV67LkkKPtkdHdc/94ylVQoRdneva0L6zr4sAdi/eEL+HLlIfyy+YSa4WRnxjXvrsCrN3ZQs7jcPQTw+m/23sAXrmmDEZ3ql7iuzLaQQFV6SGQa+3cP9kKbenFu3Z9g9s78vSppNDYyFG/cXPEfFROva4vl+86o4Ze35/lv/SIGH+XUJD5aXUqXvCxlkVog9gDlYnBS3RKu7i9pkTdboS/0Ws2ANtfbez7ysy/er9kAa5Z9cbm7JHLobC5XUn6tcLsls2KkjPVqJuSaIxASHolQFcAUF+jowUwkEBIBmEMdS0iR6663HYsEVIXus68zb8dpJJw4geGR4XilYyhwYLFjfX0bRbflsp0CDWH56UBOGmArsj+lTAf5+zVt1Ji2ZPI/P3MLptzQ3q8qfh45l6V+VYoHr2iC/i0TPP6cjeOj8e87uuKO/1uF2VtO4l+1d+OpIS0RbPkMwl+DKulx+mnjMRV0bnPJv+nYoBpGdqqLjxfswNFMqzrg39o9SQUJ+o+mqpBkY0l8FA9d0VTlApVGXr9/3tRBDdFJKfAxn63GjEf6oF71qCrvS7Bbd+g83l1gP1ncKyPbq5lEFVWjSP0i6Q1xHR7zFybNz9KS09LSUK1aNaSmpiIuzr3RssyNnj17NoYPH66KL1WEFGqSbviTaTnOSpFSYfO841K/T4o5VfYVleNhbESo+qWpByRxkfZLmcrWJzkWlzWwIFTLA6zZQH7OxUt1XS6zHZf6fdkoyM3CgT3bkZxUDyG23IuPq3+b6/w3Wn42rDlZyM3JQkhBLiKRB7PJr94e7nNJ8CIBiSxmteQWaDiTlQ+p3RhqNqNGTCQiwhxBkixS01G/rha5XfS+ouuVdx1zievZYMKC3WdwOsOKmjGRGNimDkL09VWdSYm5NBw6dBiNGjdGSKFAy/H8zkuU8pjr4xfv23YiTZ1PR94VQ9rWRRvpDXOugwpt65Lnd/7zIveVeNv17+loe4ENW7dtQ7t27RASElLmNqRrWyrc7jyZhp0nM9QpFHo3i8fAVgmwRISV8bxFbpf2eFlzn0v5t6fSc7F4zxlVjVcCEHlPShJit0Y10a9FPBrHxyC/oABr1q7HsdBE/LHztPoOkvyle/o2QbLjh1Op+17CfuxNSccbc/eownOSVzSmT2OYSvs7uGxbZgZKcSyZGi+Bx7jhrZ3D1xXdj5LWzS/Ix+o1a9Cjew+EXvIDoWrbLnndEtav0xaIjq/S9OWr316i6qZI4bCpt3au0rHssa834OdNx9VMmVmPeef0ERU5fjP4cDMJUi5kXQxOzhUJTFKzrUjLtqpLfUnLzldJg+UhQciQNokY3qGu+jIoT/XJstot+zxn20n8e/H+QnPEB7VKwEOXN0DXepE4dOo8pi/bhcXbjiBUy0UErGibEI6R7WqiY2IkzAVFAiAJamwFgC0f0AouXncutiK385Gfb0VKahZSUjORmZ2LUFMBQmBDzUgzkmtFwnTJNgoc2y5yn/PyYiEeIiKPuuUroPU1lf7nz/6wGd+uPaJ6O2Y/fnmplbTLcyyTY86Qfy1SlYwf6d/UK8MvFTl+c9jFzWR8rlZMhFrUUEc55eYXqCDkYkBSOEA5ej5L/eKUN5S8QWWRN+eQNnVUINKnaXyFy4zLr6gf1h3Bx0sPOKsUyjZu7FJfJTk1rR3jXLdx4xr4W+MmGH0hW9XCkOl0607Z8J9TQMs6sXhkQFNc3bluhXNdpKt7xf6zqlbFr7tOIMdqH7+Wnm8pqPOnrkno2LYOTJXIoVEf0F9mYfhVQxAmGywxEHK5T4a05He9XGo2pGbl4aPFe7Fsz2mYoKkg6NEBTZFcM6rQevZFbhe9r/D2Lj5eZL1ybGv/6XR8s/oQzNBUN3vLOtEX11N9ESohAAUF+di7dw+aNW3mMl7ssl6hS9fHcMm2iltffq8s3p2CI+cyERlmxtA2dRDrmA1WeH2UvC3nfS7rFHdfWbeL3Cfvp5MnTyAxMRFmvTfEZlOfmzPpOaoqp2sSnvxNZWgiPtp+0j1Zd//pTGTm2gPXyFAzGsVbkBgXWSQ7v8hvtkt+wxW3f2X/27wCDSlp2TiVlqvqaZgcPY/Vo8JQp1okqkeGFq426/i3Ns2Gc+fOo2bNGqrdkiN16EwGzmXJuXzsParSA2L/9VvC781C+2HDrpPpsBYUqN6K5nVinK9n8W0qoW2OGYL7UjJUj5zUoGlY0+LoNyh7P0rbrrrXZkOqHPDi4opU4S1p2+W+s0JtVCIufl9W1G9bT6rvdGmC5HC54xQeNf18+IXBh5+QL4XasbJElJp8ufrAOfyy5QR+33ZSRbSSGClLXGQoBrdJxNUdEtG3We1SAxHJkP98xSF8seIgzmfZv2SrW8Jw52WNcFevxqXug0TlE65ri0evbIZPlh7AFysOYdepdDz+zUa8MWe3GhO+sWv9Mrv45JwQEvj8d/0x1S2ra1o7Gjd1S8LIzvVRxx1JlDIkERIOVLKnS1Js/9qsJ1pvPoEXftqK9Wfy8NN/8/HYwEQ83L+p1857kpKeg5vfWoIzBR0wqmdDtHQURSuOzWrFzszZaNJ/OEI80MMnX/Hd8/Lxz3+vVIWmPjgSg/8+3Nvz5zwqhwKrFWtmz0an3gOwZN95zN+ZopLv9KBWyGejV5NauLJVglqSaloKbaOGTVO5FfJ+Vu/NI/b3pfxylGDf3aXm5YfHin1nMWPDMczeckINcaj9sITh5u5JuKNno0v2sbh2L3P8EjaHhakv9iaaho3rj2H8T1uRmV6A2LxQvDyyXanJokJ6bv/0wQrszc5As4QYfP9gL5ijK1+4Tvb8wp7TqiJq/gUND3Vs6rYzsOZbrVjk4d5sTzuVloNxPzpyuPo1xWVNarlt25J4fG3Heir59+kfNvnV2bs57BKgZKhEAhH5svp1qwQiuc7HJHF1cOs6GN6+Li5vEQ+zZlPtbtPzCny64gj+u+6oGtsWUqb8vr5NcFO3BpWqGCq9MhLEfLLsoOOMqUCduAjcf3kTVaDKNeFNzosj+yvBkuy76/7K9LA/dW2gTuzmri93d/+95TX++4yt+G3bSXW7Xf04vHFTJ7RMLP18KVUlv+bv+mS1SoJtlRiLmWP7lFpwyFvvc/nSHPHuMpUHdXnzeFVwyi2zvCohPceqkvWW7z2N/607gBNZhd9DUnZ+gAQbLRPQu1mtcr3XpYdEZpRMW7DXGaR3aVgdzw1rjR7JVSvmJgd4CYz+2HEKi3adVmfL1slnQH4IXN2hbrnPM1Ta31wC/Se+3aBmyAiphDnp+nYqn6y43tBRH69U60pvz38f6V2ppMfiyPeOFCgTUj9GfugY/TvdZtMw+tPVWLLnDNrWi1OJueXpwa5Iu12HX8YOaIqnh3pu+IU5H0H6Ri0rGVYPRFLSXQKRiFBc2ao2Dhw5hi3nzc6exA4NquGBfk1wVdtEtxwwpGv1m9VH8OHi/epgpP9yu7t3Mro0qq5O3Cb7Jwl+QuKLvs3iVS+H/Jr0xMncPPH3lo+LJHHJCdck8AoPMePxQc1VbQNPHXjl4CdFm+TMof/7cx80S4j1m/e5nEn3pg9WqJylOy5riEkjvHMSOum9W3PwnJpNIZdSC8YxWUWR0aYuDWvYA45WCSpoq+x+peVY8eGi/fh46X5nD4okpEpPSEUCTykcJcHG3O2nsPbQefW51UlyqFTZvbV7Q7RvUPEp7WX9zaXXVGZRyDROeV4JKP51S6dCQZS1wIYHv1ingiLpSf3h4d5oUce9gfW78/fgn3N2q8+/FCmToQAjf6d/tuwAJvxvuypN/8tjfcv8bFe23TI1/6Ev16th2BmP9Fbnb/IE5nwYjLyhejappRap0rnu8Hk1/1/GESUQ+GmTnOHUfmCUL2IJOnom13TrQUJ+Sd7TNxl3XNYIMzYcxfsL96mpqkVP1CbjztLDcUOX+qhbLfCm3slrJt3W0m3/txlbVB6OBAZSBvmNmzqU+8ujvCSofHOu/TWcOKKt27dfVe3qV1Oln2Vc+cuVh1We0Jg+pU/FrAwZ/lh94CxWHzivLqXabVGNalnQtWF1RKcfwZ9vGoiEasXN8qg46SH469CWuKtXI1W2+ps1R9Q5TObvSsENnRvgySEtiu0dkF+1G45cUAGHTFeVuguuJCCSgEOWdvWqeXSKrwTGTwxqgcub18Zfvt2Iw+eycOuHK/BI/2YqeJaKpTKdVgIPORB+cnd3twceYuyAZupv+fXqI2o2xvT7L1Ol2Y1o96l0TPl1p7r+/NWtPfrZlvpI+vCL1C/yh+EXBh9BRr7A5Pwesoy/pg02HDmP2ZuPY+/+A3j2T33Rpr5nP+jSZXhL94YqUVR6OiTR6fiFbPULR4IO+aIJhtOzS1Gvj+7qhh/XH8OE/21TpYyHv70UTw9pqYIwd1Qbla55yaWRX6rSVS7Fz/yR/G2fu6qV+iKdNGs7GteKVj0OVeldkuBCejRkeE4W17wgnSQ6yy/37sk10aNxTSRWi3T8IjzslpPrFfc3l9oLUufin3N2qXon/11/FP/bfByjezVSB3LpwZPhMQk25u08pbq6dXKA79mkphoSHdi6Tpl5HJ4gnz+ZSTHh520qyVt6Q5bsOa2CSLkt79tpt3dBNzeeI8iVfPald+xkag4W7DqtSvb/+EifEqYDByeZUrv5yAVM+mWHGv7u37K2GmbzNFV8bO8ZNbVcio95cvilPBh8BHkg0rVRTXSoF4vZs/ep+d7eIl9iEmnLEqzki/TGrg3Qp1k8nvtxMxbuOq3OACs5If+8qWOVvlDlACyFxOSg27iWRR30/Dlok960facz8N3ao+p8MyPkJH02TQVOssjsC5ntUFDguLQVs2j29Y6ey1LT1Iu+n+QA2aNxDfRIroVujWqoYkq+ICdSe29UVzUt/dVfd2Dl/nP4aMkB9Ws+32YrlNwqw579WyWo3g2ZveUPSbkxEaHq/TmgZYJKdJSqtbKIKSPbqzMae5L0wrx7exfc9tFKbD6airs/XY0xvRsjIixEzZ6KDJXLEETIdblP3XZcd1nHX4vBuZL39Z6UdGw4fAEbD19QPwalB0zTLs5Ief1PHbzy2bbPfmmHh79ajw8W7Vc/Gjw1/FIeDD6Iqkh+cUuypZSgnjRrh0p8HPbWYjw+sAVa1LEHfKpmmEshLlVmy+Qs1+R8XP8Okl/7+hkt5YtaDhj+TNoi0/qkO18OxtNXHa5yD1rnpOpqeFB6NiR/wx3VOt1JEkO/vv8yLNp9Gq/9tgs7TtirjsoQjD6cIj2QFZ0C7y2S0Nq5YXU89d0mNd1dyqLL7BpvkL/l/43ujhveX6am+UveQ0VJvpUKUBCCX1I3ok29amiVGIfWdWORVMPik+BEZqXZgwx7sLH56IVCycS6+tWj0KlhdTx8RVMkxHru1AhFDWtfF9d0qItZm0/4fPjFvz7NRAFKDr4y3NS3eW1VLEi63l/7zT6eWxXjhrVWv/gDgRxkPx7dHd+vPaJq1kj+bYjZfPHSBISEyKVUjTWpg0OhS8f9NaLDVJt9PSZd3r+7lLfv17y2GiaKiwqrUnKrt0nl0en391TJ03L6B2+SKf3T77sMHy3Zr3q6cq0FqtdIZhnl5Ltct9rUdORcq83lPDb2WiSypMOEOdtT1KKLDg9RycCt6sahteNSbhc3w6cypAaLJCIfOpupejX0YKO44UHZF+lhkEBPAlYJOhK8GHAUN/wi1XIlMHZNevY2Bh9EbiS/aL64twemrz6MGeuPqRkEQpXW0utkyX+udb0cwyw69RiknHW8KmcdSKSHxhMJp/5OAihJ+A5EEih5O/DQSd7LSyPsp4EvDzlY5roEJhnZuZg5dzGqN2qD3SmZqky+5DRIb4NMF9anF7t+PqVnRHpIWtWNRfOEWLVNCSRcizum5eSr667FHi+uU3JFaok5WyTEqiBDBRsNq6vn8MQZpytLCmD+8eQVPvub6xh8EHngy3xUz0ZqISL3kYO4zKzTj5vW6FC0rKZheO9GzimnMq344NlM7DiRrobCdp5Mx84TaepMu9IzIYvMUnNX701HR6+GDBPKNOlYN/WueJKvAw+PBh/Tpk3DP/7xD5w8eRIdO3bEO++8gx49enjq6YiIiFRCq0xblcU14T01y+o4gWC6upTgRJKkZXgvLiq00Ik81fUi98mQmuvtmMhQv+rRCDQeCT6+/fZbPPnkk/jggw/Qs2dPTJ06FUOHDsWuXbuQkOD5038TERG5krOF6/WQyPc8kob95ptv4v7778eYMWPQpk0bFYRYLBZ88sknnng6IiIiMnLPR15eHtatW4dx48Y57zObzRg0aBBWrFhxyfq5ublqcS3PKqRYkCzupG/P3dv1d2w3220URm072812+4OK7I/bz+1y/Phx1K9fH8uXL0evXr2c9z/zzDNYtGgRVq1aVWj9CRMmYOLEiZdsZ/r06aq3hIiIiPxfVlYWbr/99sA4t4v0kEh+iGvPR1JSEoYMGeKRE8vNnTsXgwcPDsiTEFUW2812G4VR2852s93+QB+5KA+3Bx/x8fEICQnBqVOnCt0vtxMTLz2DYUREhFqKkhfUUy+qJ7ftz9huYzFqu43cdrbbWML8rN0V2Re3J5yGh4eja9eumDdvnvM+m82mbrsOwxAREZExeWTYRYZRRo8ejW7duqnaHjLVNjMzU81+ISIiImPzSPBxyy234PTp0xg/frwqMtapUyf89ttvqFPHs2dLJCIiIv/nsYTTRx99VC1ERERErvzzXM9EREQUtBh8EBERkVcx+CAiIiKvYvBBREREXsXgg4iIiLzK5+XVi9JPNVORMq0VKUkrtedl2/5UFc7T2G622yiM2na2m+32B/pxuzynjPO74CM9PV1dyvldiIiIKLDIcbxatWrePattVUkpdjkzbmxsLEwmk1u3rZ+07siRI24/aZ0/Y7vZbqMwatvZbrbbH0g4IYFHvXr1YDabA6vnQ3a4QYMGHn0O+WP50x/MW9huYzFqu43cdrbbWOL8sN1l9XjomHBKREREXsXgg4iIiLzKUMFHREQEXnzxRXVpJGw3220URm072812Bxq/SzglIiKi4Gaong8iIiLyPQYfRERE5FUMPoiIiMirGHwQERGRVxkm+Jg2bRoaN26MyMhI9OzZE6tXr0YgmTJlCrp3764qvyYkJOD666/Hrl27Cq2Tk5ODsWPHolatWoiJicGNN96IU6dOFVrn8OHDuPrqq2GxWNR2nn76aeTn5xdaZ+HChejSpYvKpG7WrBk+++wz+INXX31VVb194oknDNHmY8eO4Y477lBti4qKQvv27bF27Vrn45IrPn78eNStW1c9PmjQIOzZs6fQNs6dO4dRo0apQkTVq1fHvffei4yMjELrbN68GZdffrn6bEjVxNdffx2+UlBQgBdeeAHJycmqTU2bNsWkSZMKnSsiGNq9ePFiXHvttaoSpLynZ86cWehxb7bx+++/R6tWrdQ68h6bPXs2fNV2OWfJs88+q/YjOjparXPXXXepqteB3vay/uauHnroIbXO1KlTA77dJdIM4JtvvtHCw8O1Tz75RNu2bZt2//33a9WrV9dOnTqlBYqhQ4dqn376qbZ161Zt48aN2vDhw7WGDRtqGRkZznUeeughLSkpSZs3b562du1a7bLLLtN69+7tfDw/P19r166dNmjQIG3Dhg3a7Nmztfj4eG3cuHHOdfbv369ZLBbtySef1LZv36698847WkhIiPbbb79pvrR69WqtcePGWocOHbTHH3886Nt87tw5rVGjRtrdd9+trVq1Su3j77//ru3du9e5zquvvqpVq1ZNmzlzprZp0ybtuuuu05KTk7Xs7GznOldddZXWsWNHbeXKldqSJUu0Zs2aabfddpvz8dTUVK1OnTraqFGj1Hvr66+/1qKiorR///vfmi+88sorWq1atbRZs2ZpBw4c0L7//nstJiZGe+utt4Kq3fI+fP7557Uff/xRoiptxowZhR73VhuXLVum3uuvv/66eu///e9/18LCwrQtW7b4pO0XLlxQn9Vvv/1W27lzp7ZixQqtR48eWteuXQttIxDbXtbfXCePS9vq1aun/etf/9ICvd0lMUTwIW/esWPHOm8XFBSoP+yUKVO0QJWSkqLewIsWLXJ+aOUNJF/Wuh07dqh15AOsv/nNZrN28uRJ5zrvv/++FhcXp+Xm5qrbzzzzjNa2bdtCz3XLLbeo4MdX0tPTtebNm2tz587VrrjiCmfwEcxtfvbZZ7W+ffuW+LjNZtMSExO1f/zjH8775PWIiIhQXzhCvljktVizZo1znV9//VUzmUzasWPH1O333ntPq1GjhvO10J+7ZcuWmi9cffXV2j333FPovhtuuEF9mQZru4seiLzZxptvvlm95q569uypPfjgg5o3lHYQdv3hIesdOnQoaNqOEtp99OhRrX79+ipwkB8frsFHMLTbVdAPu+Tl5WHdunWq29L1/DFye8WKFQhUqamp6rJmzZrqUtooXZau7ZRutYYNGzrbKZfSxVanTh3nOkOHDlUnKdq2bZtzHddt6Ov48rWSYRUZNim6X8Hc5p9//hndunXDTTfdpIaKOnfujI8++sj5+IEDB3Dy5MlC+y3nVJAhRde2S9esbEcn68v7f9WqVc51+vXrh/Dw8EJtlyG98+fPw9t69+6NefPmYffu3er2pk2bsHTpUgwbNiyo2+3Km230x/d+cd91MgQh7Q3mtttsNtx5551qWLht27aXPB5s7Q764OPMmTNqHNn14CPktnzAA5G8SSXvoU+fPmjXrp26T9oibzj9A1pcO+WyuNdBf6y0deRgnZ2dDW/75ptvsH79epXzUlSwtlns378f77//Ppo3b47ff/8dDz/8MB577DF8/vnnhfa9tPe1XErg4io0NFQFrBV5fbzpueeew6233qqCyLCwMBV0yXtdxrmDud2uvNnGktbx9WvgmtMlOSC33Xab8wRqwdr21157TbVDPufFCbZ2+91Zbal8PQFbt25VvwiDmZwu+vHHH8fcuXNVYpSRSIApv3AmT56sbstBWP7mH3zwAUaPHo1g9d133+Grr77C9OnT1a+/jRs3quBDkvSCud10KenVvPnmm1XyrQTiwWzdunV466231A8t6eUxgqDv+YiPj0dISMglMyDkdmJiIgLNo48+ilmzZmHBggVo0KCB835piwwxXbhwocR2ymVxr4P+WGnryK8Oybr39gcyJSVFzUKRCF+WRYsW4e2331bXJVoPtjbrZJZDmzZtCt3XunVrNXPHdd9Le1/Lpbx+rmSWj2TMV+T18SbpctZ7P2S4TLqh//KXvzh7voK13a682caS1vH1a6AHHocOHVI/PlxPGx+MbV+yZIlqkwwZ69910vannnpKzdIMxnYHffAh3fJdu3ZV48iuvyrldq9evRAoJPqXwGPGjBmYP3++moroStoo3dSu7ZRxPjlY6e2Uyy1bthR6A+sfbP1AJ+u4bkNfxxev1cCBA9X+yq9ffZHeAOmC168HW5t1MqRWdCq15EE0atRIXZe/v3xZuO63DBPJ2K9r2yUwkyBOJ+8def9L/oC+jkwBlC9717a3bNkSNWrUgLdlZWWpMWxX8uNB9jmY2+3Km230x/e+HnjI1OI//vhDTTV3FYxtv/POO9UUWdfvOuntk2Bchl2Dst2aQabaSqb4Z599pjKGH3jgATXV1nUGhL97+OGH1dS7hQsXaidOnHAuWVlZhaadyvTb+fPnq2mnvXr1UkvRaadDhgxR03VlKmnt2rWLnXb69NNPq5kj06ZN8/m0U1eus12Cuc2S4R8aGqqmnu7Zs0f76quv1D5++eWXhaZjyvv4p59+0jZv3qyNGDGi2OmYnTt3VtN1ly5dqmYNuU7Nk1kUMjXvzjvvVBn28lmR5/HVVNvRo0erbH99qq1MO5Sp0TIjKZjaLTO4ZOq3LPI1/Oabb6rr+owOb7VRpl3K++yf//yneu+/+OKLHp92WVrb8/Ly1LTiBg0aqM+r63ed6wyOQGx7WX/zoorOdgnUdpfEEMGHkNoNcpCSeh8y9VbmSQcSebMWt0jtD518MT3yyCNqqpW84UaOHKk+tK4OHjyoDRs2TM39li/1p556SrNarYXWWbBggdapUyf1WjVp0qTQc/hb8BHMbf7f//6nAicJnFu1aqV9+OGHhR6XKZkvvPCC+rKRdQYOHKjt2rWr0Dpnz55VX05SK0OmF48ZM0Z9CbqSOhIyrVe2IQd+OfD5Slpamvr7ymc1MjJS/S2kNoLrgScY2i3vt+I+zxJ8ebuN3333ndaiRQv13pcp57/88ovP2i4BZ0nfdfLvArntZf3NyxN8BGK7S2KS/3m3r4WIiIiMLOhzPoiIiMi/MPggIiIir2LwQURERF7F4IOIiIi8isEHEREReRWDDyIiIvIqBh9ERETkVQw+iIiIyKsYfBAREZFXMfggIre7++67cf311/t6N4jITzH4ICIiIq9i8EFElfbDDz+gffv2iIqKUqc+HzRokDoN+Oeff46ffvoJJpNJLQsXLlTrHzlyRJ0uvXr16qhZsyZGjBiBgwcPXtJjMnHiRNSuXRtxcXF46KGHkJeX58NWEpG7hbp9i0RkCCdOnMBtt92G119/HSNHjkR6ejqWLFmCu+66C4cPH0ZaWho+/fRTta4EGlarFUOHDkWvXr3UeqGhoXj55Zdx1VVXYfPmzQgPD1frzps3D5GRkSpgkcBkzJgxKrB55ZVXfNxiInIXBh9EVOngIz8/HzfccAMaNWqk7pNeECE9Ibm5uUhMTHSu/+WXX8Jms+Hjjz9WvSFCghPpBZFAY8iQIeo+CUI++eQTWCwWtG3bFi+99JLqTZk0aRLMZnbWEgUDfpKJqFI6duyIgQMHqoDjpptuwkcffYTz58+XuP6mTZuwd+9exMbGIiYmRi3SI5KTk4N9+/YV2q4EHjrpKcnIyFBDNkQUHNjzQUSVEhISgrlz52L58uWYM2cO3nnnHTz//PNYtWpVsetLANG1a1d89dVXlzwm+R1EZBwMPoio0mT4pE+fPmoZP368Gn6ZMWOGGjopKCgotG6XLl3w7bffIiEhQSWSltZDkp2drYZuxMqVK1UvSVJSksfbQ0TewWEXIqoU6eGYPHky1q5dqxJMf/zxR5w+fRqtW7dG48aNVRLprl27cObMGZVsOmrUKMTHx6sZLpJweuDAAZXr8dhjj+Ho0aPO7crMlnvvvRfbt2/H7Nmz8eKLL+LRRx9lvgdREGHPBxFVivReLF68GFOnTlUzW6TX44033sCwYcPQrVs3FVjIpQy3LFiwAP3791frP/vssypJVWbH1K9fX+WNuPaEyO3mzZujX79+KmlVZtRMmDDBp20lIvcyaZqmuXmbRESVInU+Lly4gJkzZ/p6V4jIg9iPSURERF7F4IOIiIi8isMuRERE5FXs+SAiIiKvYvBBREREXsXgg4iIiLyKwQcRERF5FYMPIiIi8ioGH0RERORVDD6IiIjIqxh8EBEREbzp/wH6h4AfIrhMAwAAAABJRU5ErkJggg=="
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 22
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试集"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T03:09:53.086475Z",
     "start_time": "2025-01-17T03:09:53.025878Z"
    }
   },
   "source": [
    "model.eval()\n",
    "loss = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.5304\n"
     ]
    }
   ],
   "execution_count": 23
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
